/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.simpleel.util;

import java.lang.reflect.*;
import java.util.*;
import ch.msoftch.internal.*;
import ch.simpleel.*;
import ch.simpleel.accessors.*;

/**
 * Miscellaneous utility methods.
 * 
 * @author M. Hautle
 */
public class MiscUtils {
    /** Place holder for an undefined value. */
    private static final Object UNDEFINED = new Object();

    /** Comparator for comparing {@link IValueAccessor#getPath()} against a string. */
    private static final Comparator CMP = new Comparator() {
        public int compare(Object o1, Object o2) {
            if (o1 instanceof IValueAccessor)
                return ((IValueAccessor) o1).getPath().compareTo((String) o2);
            return ((IValueAccessor) o2).getPath().compareTo((String) o1);
        }
    };

    /**
     * Creates a flat copy of the given bean.<br>
     * If the given object is not a bean (so something like immutable) the passed instance will be returned.
     * 
     * @param <T> The type
     * @param bean The bean to copy a immutable object or null
     * @return A copy, the instance itself or null
     */
    @SuppressWarnings("unchecked")
    public static <T> T copy(T bean) {
        if (bean == null)
            return null;
        try {
            final Class<T> type = (Class<T>) bean.getClass();
            // is it a immutable object (no mutable properties)?
            if (ValueAccessorManager.getMutablePropertyNames(type).length == 0)
                return bean;
            final T dst = type.newInstance();
            for (IValueAccessor p : ValueAccessorManager.getPropertyAccessors(type))
                if (p.isWritable() && p.isReadable())
                    p.setValue(dst, p.getValue(bean, null));
            return dst;
        } catch (Exception e) {
            throw new TechnicalException("Error while copying properties", e);
        }
    }

    /**
     * Copies the values from a source bean to a destination bean (of the same type).<br>
     * This is just a 'flat' copy, use {@link BeanCloner} to create a deep copy.
     * 
     * @param <T> The type
     * @param src The source bean
     * @param dst The destination bean
     * @return The destination bean
     * @throws TechnicalException If something went wrong
     */
    public static <T> T copyValues(T src, T dst) throws TechnicalException {
        return copyValues(src, dst, src.getClass());
    }

    /**
     * Copies the values from a source bean to a destination bean (of the same type).<br>
     * This is just a 'flat' copy, use {@link BeanCloner} to create a deep copy.
     * 
     * @param <T> The type
     * @param src The source bean
     * @param dst The destination bean
     * @param type The type of the beans or a supertype, only the properties defined on this type and it's supertypes will be copied
     * @return The destination bean
     * @throws TechnicalException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <T> T copyValues(T src, T dst, final Class<? extends Object> type) {
        try {
            for (IValueAccessor p : ValueAccessorManager.getPropertyAccessors(type))
                if (p.isWritable() && p.isReadable())
                    p.setValue(dst, p.getValue(src, null));
            return dst;
        } catch (Exception e) {
            throw new TechnicalException("Error while copying properties", e);
        }
    }

    /**
     * Copies the values from a source bean to a destination bean (of different type).<br>
     * This is just a 'flat' copy of properties with the same name.
     * 
     * @param <S> The source type
     * @param <T> The destination type
     * @param src The source bean
     * @param dst The destination bean
     * @param ignore The properties to ignore
     * @return The destination bean
     * @throws TechnicalException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <S, T> T copyValuesTo(S src, T dst, String... ignore) throws TechnicalException {
        try {
            final Set<String> exclude = toSet(ignore);
            final IValueAccessor[] dstProps = ValueAccessorManager.getPropertyAccessors(dst.getClass());
            for (IValueAccessor acc : ValueAccessorManager.getPropertyAccessors(src.getClass())) {
                final String name = acc.getPath();
                if (exclude.contains(name))
                    continue;
                // exists a property with the same name on the destination type?
                final int i = Arrays.binarySearch(dstProps, name, CMP);
                if (i < 0)
                    continue;
                final Object v = acc.getValue(src, UNDEFINED);
                if (v != UNDEFINED)
                    dstProps[i].setValue(dst, v);
            }
            return dst;
        } catch (Exception e) {
            throw new TechnicalException("Error while copying properties", e);
        }
    }

    /**
     * Checks wherever the given objects were equals.
     * 
     * @param o1 An object or null
     * @param o2 An object or null
     * @return True if the given objects were equals (or both null)
     */
    public static boolean equals(Object o1, Object o2) {
        // same instance or both null
        if (o1 == o2)
            return true;
        if (o1 == null)
            return false;
        return o1.equals(o2);
    }

    /**
     * Converts the given array into a set.
     * 
     * @param <T> The type
     * @param v The values
     * @return A set containing the given values
     */
    public static <T> Set<T> toSet(T... v) {
        if (v.length == 0)
            return Collections.emptySet();
        final Set<T> s = new HashSet<T>();
        Collections.addAll(s, v);
        return s;
    }

    /**
     * Expands the given array by the specified element count.
     * 
     * @param <T> The content type
     * @param array The array
     * @param size The size to add
     * @return The 'resized' array
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] arrayExpand(T[] array, int size) {
        return (T[]) arrayExpand((Object) array, size);
    }

    /**
     * Expands the given array by the specified element count.
     * 
     * @param <T> The array type
     * @param array The array
     * @param size The size to add
     * @return The 'resized' array
     */
    @SuppressWarnings("unchecked")
    public static <T> T arrayExpand(T array, int size) {
        final Class<?> type = array.getClass().getComponentType();
        final int len = Array.getLength(array);
        final T dst = (T) Array.newInstance(type, size + len);
        System.arraycopy(array, 0, dst, 0, len);
        return dst;
    }

    /**
     * Adds the passed element to the given array.
     * 
     * @param <T> The content type
     * @param array The array
     * @param value The value to add
     * @return The new array
     */
    public static <T> T[] arrayAppend(T[] array, T value) {
        final T[] dst = arrayExpand(array, 1);
        dst[array.length] = value;
        return dst;
    }

    /**
     * Adds the passed elements to the given array.
     * 
     * @param <T> The content type
     * @param array The array
     * @param values The values to add
     * @return The new array
     */
    public static <T> T[] arrayAppend(T[] array, T... values) {
        final T[] dst = arrayExpand(array, values.length);
        System.arraycopy(values, 0, dst, array.length, values.length);
        return dst;
    }

    /**
     * Adds the given beans to the map using a value of them as key.<br>
     * Simply calls {@link #putBean(Map, Object, IValueAccessor)} for each bean.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param map The map were to add the bean
     * @param keyPath The simple el path to extract the key value out of a bean
     * @param beans The beans to add
     * @return The passed map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> putBeans(Map<K, V> map, String keyPath, V... beans) {
        return putBeans(map, (IValueAccessor) ValueAccessorManager.getELAccessor(keyPath, Object.class), beans);
    }

    /**
     * Adds the given beans to the map using a value of them as key.<br>
     * Simply calls {@link #putBean(Map, Object, IValueAccessor)} for each bean.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param map The map were to add the bean
     * @param keyPath The simple el path to extract the key value out of a bean
     * @param beans The beans to add
     * @return The passed map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> putBeans(Map<K, V> map, String keyPath, Collection<V> beans) {
        return putBeans(map, (IValueAccessor) ValueAccessorManager.getELAccessor(keyPath, Object.class), beans);
    }

    /**
     * Adds the given beans to the map using a value of them as key.<br>
     * Simply calls {@link #putBean(Map, Object, IValueAccessor)} for each bean.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param map The map were to add the bean
     * @param keyAccessor The accessor to extract the key value out of the bean
     * @param beans The beans to add
     * @return The passed map
     */
    public static <K, V> Map<K, V> putBeans(Map<K, V> map, IValueAccessor<K> keyAccessor, V... beans) {
        for (V b : beans)
            putBean(map, b, keyAccessor);
        return map;
    }

    /**
     * Adds the given beans to the map using a value of them as key.<br>
     * Simply calls {@link #putBean(Map, Object, IValueAccessor)} for each bean.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param map The map were to add the bean
     * @param keyAccessor The accessor to extract the key value out of the bean
     * @param beans The beans to add
     * @return The passed map
     */
    public static <K, V> Map<K, V> putBeans(Map<K, V> map, IValueAccessor<K> keyAccessor, Collection<V> beans) {
        for (V b : beans)
            putBean(map, b, keyAccessor);
        return map;
    }

    /**
     * Adds a bean to a map using a value of it as key.<br>
     * This method used {@link IValueAccessor#getValue(Object, Object)} to extract the key of the bean.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param map The map were to add the bean
     * @param bean The bean to add
     * @param keyAccessor The accessor to extract the key value out of the bean
     * @return The old value for the key or null
     */
    public static <K, V> V putBean(Map<K, V> map, V bean, IValueAccessor<K> keyAccessor) {
        return map.put(keyAccessor.getValue(bean, null), bean);
    }

    /**
     * Evaluates the given el path on each element of the collection and returns the first one which matches with the given key.
     * 
     * @param <T> The element type
     * @param list The collection
     * @param path The simple el path
     * @param key The key value
     * @return The first matching element or null
     */
    public static <T> T getFirst(Collection<T> list, String path, Object key) {
        return getFirst(list, ValueAccessorManager.getELAccessor(path, Object.class), key);
    }

    /**
     * Evaluates the given el paths on each element of the collection and returns the first one which matches with all given keys.
     * 
     * @param <T> The element type
     * @param list The collection
     * @param paths The simple el paths
     * @param keys The key values
     * @return The first matching element or null
     */
    @SuppressWarnings("unchecked")
    public static <T> T getFirst(Collection<T> list, String[] paths, Object... keys) {
        final IValueAccessor<T>[] accessors = new IValueAccessor[paths.length];
        for (int i = 0; i < accessors.length; i++)
            accessors[i] = (IValueAccessor<T>) ValueAccessorManager.getELAccessor(paths[i], Object.class);
        return getFirst(list, accessors, keys);
    }

    /**
     * Applies the given accessor on each element of the collection and returns the first one which matches with the given key.
     * 
     * @param <T> The element type
     * @param <K> The key type
     * @param list The collection
     * @param accessor The accessor
     * @param key The key value
     * @return The first matching element or null
     */
    public static <T, K> T getFirst(Collection<T> list, IValueAccessor<K> accessor, K key) {
        if (list instanceof List)
            return getFirst((List<T>) list, accessor, key);
        for (T e : list)
            if (equals(accessor.getValue(e, null), key))
                return e;
        return null;
    }

    /**
     * Applies the given accessor on each element of the list and returns the first one which matches with the given key.
     * 
     * @param <T> The element type
     * @param <K> The key type
     * @param list The collection
     * @param accessor The accessor
     * @param key The key value
     * @return The first matching element or null
     */
    private static <T, K> T getFirst(final List<T> list, final IValueAccessor<K> accessor, final K key) {
        for (int i = 0, cnt = list.size(); i < cnt; i++) {
            final T e = list.get(i);
            if (equals(accessor.getValue(e, null), key))
                return e;
        }
        return null;
    }

    /**
     * Applies the given accessor on each element of the collection and returns the first one which matches with all given keys.
     * 
     * @param <T> The element type
     * @param list The collection
     * @param accessors The accessors
     * @param keys The key values
     * @return The first matching element or null
     */
    @SuppressWarnings("unchecked")
    public static <T> T getFirst(Collection<T> list, IValueAccessor[] accessors, Object... keys) {
        if (accessors.length != keys.length)
            throw new IllegalArgumentException("The number of accessors must be equals to the number of keys!");
        if (list instanceof List)
            return getFirst((List<T>) list, accessors, keys);
        check: for (T e : list) {
            for (int i = 0; i < accessors.length; i++)
                if (!equals(accessors[i].getValue(e, null), keys[i]))
                    continue check;
            // all keys were equal - return the current element
            return e;
        }
        return null;
    }

    /**
     * Applies the given accessor on each element of the list and returns the first one which matches with all given keys.
     * 
     * @param <T> The element type
     * @param list The list
     * @param accessors The accessors
     * @param keys The key values
     * @return The first matching element or null
     */
    @SuppressWarnings("unchecked")
    private static <T> T getFirst(List<T> list, IValueAccessor[] accessors, Object... keys) {
        check: for (int i = 0, cnt = list.size(); i < cnt; i++) {
            final T e = list.get(i);
            for (int j = 0; j < accessors.length; j++)
                if (!equals(accessors[j].getValue(e, null), keys[j]))
                    continue check;
            // all keys were equal - return the current element
            return e;
        }
        return null;
    }

    /**
     * Gets or creates the set for the given key.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param key The key
     * @param map The map
     * @return The set for the key
     */
    public static <K, V> Set<V> getOrCreateSet(K key, Map<K, Set<V>> map) {
        Set<V> list = map.get(key);
        if (list == null)
            map.put(key, list = new HashSet<V>());
        return list;
    }

    /**
     * Gets or creates the list for the given key.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param key The key
     * @param map The map
     * @return The list for the key
     */
    public static <K, V> List<V> getOrCreateList(K key, Map<K, List<V>> map) {
        List<V> list = map.get(key);
        if (list == null)
            map.put(key, list = new ArrayList<V>());
        return list;
    }

    /**
     * Gets or creates the list for the given key.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param key The key
     * @param map The map
     * @param size The initial size for new lists
     * @return The list for the key
     */
    public static <K, V> List<V> getOrCreateList(K key, Map<K, List<V>> map, int size) {
        List<V> list = map.get(key);
        if (list == null)
            map.put(key, list = new ArrayList<V>(size));
        return list;
    }
}
